Repository navigation
fix(formula,lint,mcp): a fail-closed date verdict says the write is refused, and a time-relative sweep's run-fails verdict names no firing write - #30492
Conversation
…lative sweep sentence (red) Claude-Session: https://claude.ai/code/session_01Utp6CbntgiC5D1vTyg2pBV Co-authored-by: Claude <noreply@anthropic.com>
…efused; a sweep's run-fails verdict names no firing write Claude-Session: https://claude.ai/code/session_01Utp6CbntgiC5D1vTyg2pBV Co-authored-by: Claude <noreply@anthropic.com>
… three codes Claude-Session: https://claude.ai/code/session_01Utp6CbntgiC5D1vTyg2pBV Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 25 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a2228ffd2adab4c9c61b4cdfe76eb1ce044fe514 && git checkout a2228ffd2adab4c9c61b4cdfe76eb1ce044fe514
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0c7e95aff5996de2313b4668760eafee2e50172d fc42e4af8618938b74b1720f114c5931362dc2c4 && git checkout -B drift-repro 0c7e95aff5996de2313b4668760eafee2e50172d && git merge --no-ff fc42e4af8618938b74b1720f114c5931362dc2c4
node scripts/docs-audit/affected-docs.mjs --json 0c7e95aff5996de2313b4668760eafee2e50172d
|
|
domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
|
domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb 席位核对 · ACCEPTHead
Marking ready and arming auto-merge. Main is red at the moment (p0 #30467): if the merge group bounces on that, this PR re-queues once it is fixed. |
|
domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb 维护者速读
席位意见:同意落地。这是放宽型改动(新增码和可选键),declaration 面记录 PASS(6659206175),钉住 head 席位核对Head
|
|
domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb Requeue · 2026-11-05T14:52ZThe merge group
This is not this PR's failure. Requeued once. If the same test fails again in the next group, this re-queue is the only one, and the failure is treated as real and investigated. |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 42045320193 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Requested by Bill · project thread
domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb
维护者速读
date-arithmetic-fail-closed/date-text-arithmetic-fail-closed。二是定时相对扫描流程的条件里文本和数字比大小,判词不再说「触发它的写入照常落库」(扫描没有写入),改说「这一行的运行失败,别的什么都不发生」,新码type-mismatch-sweep-run-fails。date-arithmetic/date-text-arithmetic识别失败即拒站点、按type-mismatch-run-fails识别扫描流程的使用方需改认新码。公式字段、advisory 校验规则、记录触发流程原码原句不动。回滚即 revert,无数据迁移。packages/formula/src/validate.ts(dateArm/dateArmFault和TYPE_MISMATCH_SWEEP_ARM)、packages/lint/src/validate-expressions.ts(FlowBareNames.timeRelativeSweep)、packages/mcp/src/mcp-http-tools.ts(一行传键)。pnpm --filter @objectstack/lint exec vitest run src/validate-expressions.test.ts -t 30250,预期 2 条通过:扫描流程判词不含 still lands,记录触发对照仍含;失败即拒规则的日期判词说写入被拒,公式字段对照仍说 null。Fixes #30250
Clause-②: yes (widening) — three codes join
@objectstack/formula's exportedExpressionRefusalCode/EXPRESSION_REFUSAL_CODES(date-arithmetic-fail-closed,date-text-arithmetic-fail-closed,type-mismatch-sweep-run-fails) and the optional keytimeRelativeSweepjoins the exportedExprSchemaHint. A fail-closed site's date verdicts move to the first two fromdate-arithmetic/date-text-arithmetic, and a time-relative sweep's type-mismatch verdict moves to the third fromtype-mismatch-run-fails(a consumer keyed to the old codes at those sites re-keys). Grading is unchanged at every site; what is refused, warned or passed is unchanged.Metadata-protocol: none — what an author can write is unchanged, and
os validatereports no expression refusal code: its findings come from@objectstack/lint'sExprIssue(where,message,source,severity,path), which carries nocode. Only the message text changes there.Skills-docs: none —
skills/**grep forstill landsandsilently evaluates to nullhitsskills/objectstack-formula/SKILL.md:407andskills/objectstack-formula/evals/cel-predicates-formulas.json:87, both about a record-triggered flow's null-guard edge, which still lands the write, so both stay true. The one hand-written doc sentence this PR makes false is fixed here:content/docs/data-modeling/formulas.mdx(type-soundness row).What changes
Measured on
mainatc5800818before the change, and again afterorigin/mainwas merged.How each site learns its scope. (1) A fail-closed site already says so:
@objectstack/lint'sfaultOutcomeOfledger and MCPvalidate_expression(sitevalidation) passfaultOutcome: 'fail-closed'at arecord-scoped site, and a formula field passes none. So the date verdicts read it with no new signal. (2) A time-relative sweep did not say so. The ledger answersrun-failsfor a record-triggered flow and for a sweep of the typed object alike, so@objectstack/formulacould not tell them apart. The new optionalExprSchemaHint.timeRelativeSweepcarries that one fact. It is read only underfaultOutcome: 'run-fails'at a'flattened'site.recordsite (errorvalidation rule, fieldrequiredWhen/readonlyWhen, optionvisibleWhen, hookcondition): date arithmeticdate-arithmetic/date-text-arithmetic, error, "this faults at runtime, so the field silently evaluates to null."date-arithmetic-fail-closed/date-text-arithmetic-fail-closed, error: "this faults at runtime, and here an expression that cannot be evaluated fails closed: the write (or the operation a hook guards) is refused, not skipped (it does not read null)."warning/info) validation rule: date arithmeticdate-arithmetic/date-text-arithmeticrun-fails): text or boolean against a numbertype-mismatch-run-fails, error, "… the flow stops at this condition, the write that fired it still lands, and the work the flow exists to do is not done."type-mismatch-sweep-run-fails, error: "… a flow condition that cannot be evaluated fails the run: the time-relative sweep starts one run per row it finds, the run for this row stops at this condition and the work the flow exists to do is not done for that row, and nothing else happens (the sweep logs the failure and goes on to the next row)."run-fails)type-mismatch-run-failsWhy new codes. A code names one message template (
expression-refusal.ts, the contract), and each new sentence is a new template, as #30015 and #30016 did. The sweep sentence is read offpackages/triggers/trigger-schedule/src/time-relative-trigger.ts: each matched row is launched with{ record: row, event: 'time_relative' }, and a failed run is caught per row, logged aterrorand counted, and the sweep goes on.The nullable-comparison sentence (
validate-null-guards.tsOUTCOME_CLAUSE['run-fails']) is unchanged. It is never served on a sweep:validate-expressions.tsruns the null-guard twin only wheretriggerKind === 'record_change'(nullGuardObject), and no other caller passesrun-failstocheckNullGuards/nullGuardMessage. On a record-triggered flow it is true. The new lint pin asserts that no finding on a sweep says "still lands", and that the record-triggered control still does.Routing.
@objectstack/lint:FlowBareNamesgainstimeRelativeSweep(triggerKind === 'time_relative'), handed to the validator at every flow site that already gets the bare-name facts (start, edge,decision, inert node conditions).@objectstack/mcp:validate_expressionwithsite: 'flow_condition'andtriggerKind: 'time_relative'passes the same key, so the tool's answer stays the gate's (the gate-parity test compares messages). The date verdicts'-run-failssentence already names no write, so it is served on a sweep as it is.Tests
packages/formula/src/validate.test.ts:recordsite'srecord.start_date + 1 > 3andrecord.end_date - record.start_date > duration("24h")get the-fail-closedcodes and the refused-write sentence, and not "evaluates to null". Controls: a formula field (value, no outcome) and an advisory rule (fail-open) keepdate-arithmetic/date-text-arithmeticand the null sentence;name > 5andrecord.name > 5underrun-fails+timeRelativeSweepgettype-mismatch-sweep-run-fails, the per-row sentence, and no "still lands" / "write that fired". With no outcome the key changes nothing (type-mismatch-run-may-fail). A sweep's date verdict staysdate-arithmetic-run-fails;errors with the run-fails wording where the caller declares a flow condition (#29926)(the record-triggered "still lands" pin,:801onmain).packages/formula/src/expression-refusal-codes.test.ts: aPINSentry for each new code (exact params and message).SWEEP_SCHEMASgains a fail-closedrecordschema with a date field and a flattenedrun-fails+timeRelativeSweepschema, so the sweep reaches all three codes and holds their params to their messages.packages/lint/src/validate-expressions.test.ts:title > 5gets the per-row sentence, and no finding on the flow says "still lands" (nor does the null-guard twin, which is not served there). Control: the same flow record-triggered keeps "the write that fired it still lands" on the type verdict and on the null-guard verdict of its edge;#30016 control, which pinned the null sentence on a fail-closed rule): twoerrorvalidation rules with date arithmetic say the write is refused, not null. Control: a formula field'srecord.close_date + 1keeps the null sentence;objectName— a flow naming onlytimeRelative.objectgets no type verdict; MCP assumes the sweep object; #30015 wording on an omitted object is false #30026 time-relative test asserted "the write that fired it still lands" on the sweep's three errors. It now asserts the per-row sentence and the absence of "still lands" / "write that fired".packages/mcp/src/mcp-validate-expression.test.ts: unchanged. Its gate-parity rows fortriggerKind: 'time_relative'compare the tool's messages with the gate's, so they pin the MCP half (ablation A2 below).packages/objectql/src/publish-type-mismatch-fail-closed.test.ts: unchanged, 12/12 passed.Suites (head fc42e4a, under os-verify-lock, shared box)
Ablations (scripts/ablation-replace.mjs; each restored: blob == HEAD, git diff HEAD empty, dist marker absent after rebuild)
Acceptance notes
warning/info) validation rule's date arithmetic still says "silently evaluates to null". There a fault skips the rule and the write proceeds (type-mismatch-fail-open's sentence), so "evaluates to null" is not true either. Out of this card's ruling (fail-closed only), left as is and pinned as a control. Reported to the seat as a finding in the same family.content/docs/data-modeling/formulas.mdxdate-arithmetic row says "this always faulted tonull". That describes a formula's runtime, and this PR changes no runtime, so it is left as is.packages/formula/src/validate.ts,packages/lint/src/validate-expressions.tsorpackages/mcp/src/mcp-http-tools.tswere not enumerated by this dev (the seat's claim cleared them at 12:18Z).origin/mainwas merged once atfc42e4afwith no conflict, and none of the 8 incoming commits touches these packages.Verification
All at head
fc42e4af(git rev-parse --short HEAD), the commit this PR opens on (origin/mainmerged once).pnpm install --frozen-lockfile && turbo run build --filter='!@objectstack/docs' --concurrency=2, 119/119 tasks,VERDICT command-exit 0.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 88 derived families, each run on this head, all exit 0.check-plugin-teardown-shape.mjs --self-testfirst answered exit 3 because the shallow clone could not read its pinned fixture commit621a4876; after fetching that commit it answered exit 0 ("48 cases pass"), and the record carries that run.--ran: "88 derived famil(ies) accounted for — 88 run, 0 NOT-MEASURED". Among themcheck-adr-0087-registration --base origin/main,check-changeset-no-major,check-empty-changeset,check:doc-authoring,check:docs-transcript-drift,check:test-source-alias,check:cross-package-test-inputs,check:dts-closure,check:dual-build-cjs-loads,check:lean-entry-closure,check:nul-bytes,lint check:doc-formula-expressions.pnpm lint(eslint . --no-inline-config): exit 0.!andClause-②has no(narrowing)arm.🤖 Generated with Claude Code
https://claude.ai/code/session_01Utp6CbntgiC5D1vTyg2pBV